package uk.co.caprica.vlcj.oop.component;

import gnu.cajo.invoke.Remote;
import gnu.cajo.utils.extra.TransparentItemProxy;

import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;

import uk.co.caprica.vlcj.logger.Logger;
import uk.co.caprica.vlcj.oop.OutOfProcessMediaPlayer;
import uk.co.caprica.vlcj.oop.registry.RemotingRegistry;

/**
 * Implementation of a rendezvous for creating and getting a handle to an out-
 * of-process media player.
 * <p>
 * A rendezvous instance is created each time an out-of-process media player is
 * created and is bound to the remote naming registry in the main application.
 * <p>
 * This class is not responsible for binding itself in the remote naming 
 * registry - that responsibility lies elsewhere.
 * <p>
 * The external process, when it has finished its own initialisation, looks up
 * the rendezvous object in the naming registry and remotely invokes the 
 * {@link #initialise(String, int)} method to register the name and port of the
 * host on which the remote media player resides.
 * <p>
 * The naming registry on this remote host and port is then queried to get a
 * transparent proxy to the out-of-process media player instance.
 * <p>
 * Whilst this synchronisation protocol may seem complex, it is entirely 
 * necessary.
 * <p>
 * The caller may request the reference to the out-of-process media player at
 * entire time by invoking {@link #mediaPlayer()}. If the remote media player
 * is ready then this method will return immediately, otherwise it will <em>block</em>
 * until the remote process is ready.
 * <p>
 * <strong>All public methods exposed by this class may be invoked by a remote process.</strong>
 */
final class OutOfProcessMediaPlayerRendezvous {

    /**
     * Name under which the media player proxy is bound in the remote naming registry. 
     */
    private static final String MEDIA_PLAYER_OBJECT_NAME = "MediaPlayer";
    
    /**
     * Latch used to wait until the remote process has signalled that it is ready.
     */
    private final CountDownLatch remoteReadyLatch = new CountDownLatch(1);
    
    /**
     * Associated media player component.
     */
    private final OutOfProcessMediaPlayerComponent mediaPlayerComponent;
    
    /**
     * Future task to get a reference to the remote media player.
     */
    private final FutureTask<OutOfProcessMediaPlayer> mediaPlayerFuture;
    
    /**
     * Name of the host on which the remote media player resides.
     */
    private String remoteHost;
    
    /**
     * Port number at which the remote media player resides.
     */
    private int remotePort;
    
    /**
     * Create a rendezvous.
     */
    OutOfProcessMediaPlayerRendezvous(OutOfProcessMediaPlayerComponent mediaPlayerComponent) {
        this.mediaPlayerComponent = mediaPlayerComponent;
        mediaPlayerFuture = new FutureTask<OutOfProcessMediaPlayer>(new InitialiseMediaPlayerCallable());
    }

    /**
     * Get the {@link Future} that provides the out-of-process media player reference.
     * 
     * @return future
     */
    FutureTask<OutOfProcessMediaPlayer> mediaPlayerFuture() {
        return mediaPlayerFuture;
    }
    
    /**
     * Initialisation callback.
     * <p>
     * This method is remotely invoked by the external media player process when it is ready.
     * 
     * @param remoteHost the name of the host on which the remote media player resides
     * @param remotePort the port number at which the remote media player resides
     */
    public final void initialise(String remoteHost, int remotePort) {
        Logger.debug("initialise(remoteHost={},remotePort={})", remoteHost, remotePort);
        this.remoteHost = remoteHost;
        this.remotePort = remotePort;
        Logger.debug("Count down remote ready latch...");
        remoteReadyLatch.countDown();
        Logger.debug("Remote ready latch counted down.");
    }

    /**
     * Ping this remote object.
     */
    public final void ping() {
        Logger.trace("ping()");
        // No implementation
    }
    
    /**
     * Get a reference to the remote media player.
     * <p>
     * The returned media player reference will be a transparent proxy to the remote
     * media player.
     * <p>
     * This call will <em>block</em> until the remote media player is available.
     * 
     * @return media player
     */
    public final OutOfProcessMediaPlayer mediaPlayer() {
        Logger.trace("mediaPlayer()");
        try {
            // FIXME timeout?
            return mediaPlayerFuture.get();
        }
        catch(InterruptedException e) {
            throw new RuntimeException("Interrupted while waiting for remote media player to be initialised", e);
        }
        catch(ExecutionException e) {
            throw new RuntimeException("Failed to get a reference to the remote media player", e);
        }
    }
    
    /**
     * Release the resources associated with this component.
     */
    public final void release() {
        Logger.debug("release()");
        try {
            String remoteProcessAddress = RemotingRegistry.objectName(remoteHost, remotePort, "MediaPlayerProcess"); // FIXME const enum or something
            Logger.debug("remoteProcessAddress={}", remoteProcessAddress);
            Object object = Remote.getItem(remoteProcessAddress);
            Remote.invoke(object, "terminate", null); // FIXME const enum or something
        }
        catch(Exception e) {
            Logger.warn("Failure releasing remote process", e.getMessage());
        }
    }
    
    /**
     * Implementation of a {@link Callable} that waits for the remote media player
     * to be ready and obtains a reference to that remote media player.
     * <p>
     * The returned reference is a transparent proxy to the remote media player.
     */
    private final class InitialiseMediaPlayerCallable implements Callable<OutOfProcessMediaPlayer> {

        @Override
        public OutOfProcessMediaPlayer call() throws Exception {
            Logger.debug("call()");
            // Wait for the external process to notify that it is ready...
            Logger.debug("Waiting for remote ready latch...");
            remoteReadyLatch.await();
            Logger.debug("Remote ready latched.");
            // The external process is ready, get a proxy to the remote media player
            String remoteMediaPlayerAddress = RemotingRegistry.objectName(remoteHost, remotePort, MEDIA_PLAYER_OBJECT_NAME);
            Logger.debug("remoteMediaPlayerAddress=" + remoteMediaPlayerAddress);
            OutOfProcessMediaPlayer outOfProcessMediaPlayer = (OutOfProcessMediaPlayer)TransparentItemProxy.getItem(remoteMediaPlayerAddress, new Class[] {OutOfProcessMediaPlayer.class});
            Logger.debug("remoteMediaPlayer={}", outOfProcessMediaPlayer);
            // Notify the associated media player component that the process is ready
            mediaPlayerComponent.notifyProcessReady();
            // Finally, return the out-of-process media player proxy
            return outOfProcessMediaPlayer;
        }
    }
}
